package com.meego.common.crypto;

import com.meego.common.log.GA;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;

public class SignedInputStream extends InputStream {

    private Cipher cipher;
    private MessageDigest digest;
    private byte[] encrypt;
    private byte[] decrypt;
    private int pos;
    private String algorithm = "DESede";
    private String fullAlgorithm = "DESede/CBC/NoPadding";
    private InputStream in;
    private boolean done;

    public SignedInputStream(InputStream aSource, char[] key)
            throws IOException {
        this.in = aSource;
        try {
            this.cipher = Cipher.getInstance(this.fullAlgorithm);

            byte[] bits = new byte[24];

            for (int i = 0; i < 24; i++) {
                bits[i] = (byte) key[(i % key.length)];
            }

            this.cipher.init(Cipher.DECRYPT_MODE, SecretKeyFactory.getInstance(this.algorithm).generateSecret(new DESedeKeySpec(bits)),
                    new IvParameterSpec(new byte[8]));

            this.digest = MessageDigest.getInstance("MD5");
        } catch (Exception ex) {
            GA.crypto.error("Failure in creating a new instance of SignedInputStream using the given key", ex);
            throw new IOException(ex.toString());
        }

        int size = this.cipher.getBlockSize();
        if (size == 0) {
            this.encrypt = new byte[8];
        } else {
            this.encrypt = new byte[size];
        }
    }

    @Override
    public int read() throws IOException {
        if (this.done) {
            return -1;
        }
        int result = readCipher();
        if (result == -1) {
            throw new EOFException("No EOF Marker");
        }
        if (result == 255) {
            result = readCipher();
            if (result == -1) {
                throw new EOFException("No EOF Marker");
            }
            if (result == 255) {
                this.digest.update((byte) result);
                return 255;
            }

            if (result == 0) {
                ByteArrayOutputStream target = new ByteArrayOutputStream();
                while (true) {
                    result = readCipher();
                    if (result == -1) {
                        throw new EOFException("No EOF Marker");
                    }
                    if (result == 255) {
                        result = readCipher();
                        if (result == -1) {
                            throw new EOFException("No EOF Marker");
                        }
                        if (result == 255) {
                            target.write(result);
                            continue;
                        }
                        if (result == 0) {
                            byte[] c1 = target.toByteArray();
                            byte[] c2 = this.digest.digest();
                            if (c1.length != c2.length) {
                                throw new IOException("Invalid file "
                                        + c1.length + ":" + c2.length);
                            }
                            for (int scan = 0; scan < c1.length; scan++) {
                                if (c1[scan] != c2[scan]) {
                                    throw new IOException("Invalid file");
                                }
                            }
                            this.done = true;
                            return -1;
                        }
                    }
                    target.write(result);
                }
            }
            throw new IOException("Invalid file");
        }
        this.digest.update((byte) result);
        return result;
    }

    @Override
    public void close() throws IOException {
        this.in.close();
    }

    private int readCipher() throws IOException {
        while (true) {
            if (this.decrypt == null) {
                int read = this.in.read(this.encrypt);
                if (read == 0) {
                    return -1;
                }
                if (read == -1) {
                    return -1;
                }
                try {
                    this.decrypt = this.cipher.update(this.encrypt, 0, read);
                } catch (IllegalStateException ex) {
                    throw new IOException("Illegal State");
                }
                this.pos = 0;
            }

            if (this.pos < this.decrypt.length) {
                int result = this.decrypt[(this.pos++)];
                if (result < 0) {
                    result += 256;
                }
                return result;
            }
            this.pos = 0;
            this.decrypt = null;
        }
    }
}
